<?php
/**
*	@author SignpostMarv Martin
*	@package UHU_components
*	@subpackage LLSD
*/

/**
*	The Exception class thrown by uhu_LLSD components
*	@package UHU_exception
*	@subpackage LLSD
*/
class uhu_LLSD_exception extends uhu_exception
{
/**
*	@param int|null $code taken from $this->getCode()
*	@return string an extended error message
*/
	protected function getExtendedMessage($code=null)
	{
		$message = '';
		switch($code)
		{
			case 100: # Used by DOMDocument
			case 101: # Used by SimpleXML
				$message = 'Could not parse LLSD document: %s';
			break;

			case 102: # Validation problem
			case 103: # Invalid UUID
				$message = 'Could not validate LLSD document: %s';
			break;

			case 104: # Unsupported binary encoding.
			case 105: # Invalid date format.
			case 106: # Problems with map element
			case 107: # Problems with integer element
			case 108: # Problems with real element
				$message = 'Problem while parsing LLSD document: %s';
			break;

			case 200:
				$message = 'Problem generating LLSD document: %s';
			break;
		}
		return $message;
	}
}

/**
*	Relatively pointless abstract class, only present to make it easier to do instanceof checks
*	@package LLSD
*/
abstract class uhu_LLSD_common{}

uhu::load_components('LLSD struct','LLSD binary','LLSD date','LLSD numeric','LLSD uri','LLSD UUID');	# Loads the necessary sub-components

/**
*	The main class for doing LLSD-based operations.
*	@package LLSD
*	@uses uhu_LLSD_common
*	@uses SimpleXMLElement
*	@uses DOMDocument
*/
class uhu_LLSD extends uhu_LLSD_common
{
/**
*	A modified version of the LLSD DTD that doesn't cause any errors to be thrown when passed to DOMDocument
*/
	const LLSD_DTD = "<!DOCTYPE llsd [\n<!ELEMENT llsd (undef|boolean|integer|real|uuid|string|date|uri|binary|map|array)>\n<!ELEMENT DATA (undef|boolean|integer|real|uuid|string|date|uri|binary|map|array)>\n<!ELEMENT ATOMIC (undef|boolean|integer|real|uuid|string|date|uri|binary)>\n<!ELEMENT KEYDATA (key,(undef|boolean|integer|real|uuid|string|date|uri|binary|map|array))>\n<!ELEMENT key (#PCDATA)>\n<!ELEMENT map ((key,(undef|boolean|integer|real|uuid|string|date|uri|binary|map|array))*)>\n<!ELEMENT array ((undef|boolean|integer|real|uuid|string|date|uri|binary|map|array)*)>\n<!ELEMENT undef (EMPTY)>\n<!ELEMENT boolean (#PCDATA)>\n<!ELEMENT integer (#PCDATA)>\n<!ELEMENT real (#PCDATA)>\n<!ELEMENT uuid (#PCDATA)>\n<!ELEMENT string (#PCDATA)>\n<!ELEMENT date (#PCDATA)>\n<!ELEMENT uri (#PCDATA)>\n<!ELEMENT binary (#PCDATA)>\n\n<!ATTLIST string xml:space (default|preserve) 'preserve'>\n<!ATTLIST binary encoding (base64|base16|base85) 'base64'>\n]>";
/**
*	I forget what this does. :-P
*/
	const LLSD_numeric_paranoia = 2;
/**
*	@var SimpleXMLElement An instance of a SimpleXMLElement object, generated from the XML supplied by {@link uhu_LLSD::__construct()}
*/
	protected $document;
/**
*	@var array the placeholder for the native PHP representation of the LLSD document
*/
	protected $struct = array();
/**
*	I forget what this does :-P
*/
	protected $params;
/**
*	Builds an instance of uhu_LLSD from source, with optional custom DTD (custom DTD is useful with {@link http://wiki.secondlife.com/wiki/RegAPI/DTDs LL's RegAPI})
*	@param string the LLSD document to be parsed
*	@param bool|string if false, {@link uhu_LLSD::LLSD_DTD} is used, otherwise the value is passed to DOMDocument when doing validation of the LLSD document.
*	@uses uhu_LLSD::validate()
*/
	public function __construct($source,$custom_dtd=false)
	{
		if(self::validate($source,$custom_dtd))
		{
			$this->document = simplexml_load_string($source);
			$this->struct = new uhu_LLSD_struct($this->document);
		}
		else
		{
			throw new uhu_LLSD_exception('LLSD document is invalid.',101);
		}
	}
/**
*	Returns the native PHP version of the LLSD document.
*	@uses uhu_LLSD::$struct
*/
	public function struct()
	{
		return $this->struct;
	}
/**
*	Converts a native PHP data type into an LLSD document.
*	@param mixed The native PHP version of an LLSD document
*	@param null|integer If null, no indentation is used, otherwise the number of specified tabs is used as the starting indents (0 is suggested)
*	@uses uhu_LLSD::struct_build_LLSD()
*	@uses uhu_LLSD::level_based_indent();
*/
	public static function save($struct,$indent=null)
	{
		$doc = '<llsd>';
		$doc .= self::struct_build_LLSD($struct,is_null($indent) ? null : (integer)$indent);
		return $doc .= self::level_based_indent(is_null($indent) ? null : 0) . '</llsd>';
	}
/**
*	Converts a native PHP data type into an LLSD node (note, it uses short-form versions of various data types where appropriate).
*	@param mixed The native PHP data type to be converted
*	@param null|integer If null, no indentation is used, otherwise the number of specified tabs is used as the starting indents
*	@return string A string representation of the native PHP data type as an LLSD node
*	@uses uhu_LLSD::is_map()
*	@uses uhu_LLSD::level_based_indent()
*	@uses uhu::struct_build_LLSD()
*	@uses uhu_LLSD_exception
*/
	protected static function struct_build_LLSD($node,$level=null)
	{
		$doc = '';
#		if($node instanceof uhu_LLSD_struct)
#		{
#			$node = $node->current();
#		}
		if(is_array($node) || (is_object($node) && $node instanceof Iterator))	# the Iterator interface can be used with objects in order to toss objects at the function
		{
			if(self::is_map($node))
			{
				$doc .= self::level_based_indent($level) . '<map>';
				foreach($node as $label => $sub_node)
				{
					$doc .=
						self::level_based_indent(is_integer($level) ? $level + 1 : null) . '<key>' . (string)$label . '</key>' . self::struct_build_LLSD($sub_node,is_integer($level) ? $level + 1 : null);
				}
				$doc .= self::level_based_indent($level) . '</map>';
			}
			else
			{
				$doc .= self::level_based_indent($level) . '<array>';
				foreach($node as $sub_node)
				{
					$doc .= self::struct_build_LLSD($sub_node,is_null($level) ? null : $level + 1);
				}
				$doc .= self::level_based_indent($level) . '</array>';
			}
		}
		else if(is_object($node))
		{
			switch(get_class($node))
			{
				case 'uhu_LLSD_UUID':
					$doc .= self::level_based_indent($level) . (((string)$node !== NULL_KEY) ? '<uuid>' . (string)$node . '</uuid>' : '<uuid />');
				break;
				case 'uhu_LLSD_integer_NaN':
					$doc .= self::level_based_indent($level) . '<integer>nan</integer>';
				break;
				case 'uhu_LLSD_real_NaN':
					$doc .= self::level_based_indent($level) . '<real>nan</real>';
				break;
				case 'uhu_LLSD_uri':
					$doc .= self::level_based_indent($level) . ((strlen((string)$node) === 0) ? '<uri />' : '<uri>' . (string)$node . '</uri>');
				break;
				case 'uhu_LLSD_date':
					$doc .= self::level_based_indent($level) . (($node->unix() === 0) ? '<date />' : '<date>' . date('c',$node->unix()) . '</date>');
				break;
				default:
					throw new uhu_LLSD_exception('Unsupported object type provided: ' . get_class($node),200);
				break;
			}
		}
		else if(is_string($node))
		{
			$doc .= self::level_based_indent($level) . '<string>' . (string)$node . '</string>';
		}
		else if(is_float($node))
		{
			$doc .= self::level_based_indent($level) . '<real>' . (string)$node . '</real>';
		}
		else if(is_bool($node))
		{
			$doc .= self::level_based_indent($level) . ($node ? '<boolean>1</boolean>' : '<boolean />');
		}
		else if(is_integer($node))
		{
			$doc .= self::level_based_indent($level) . (($node === 0) ? '<integer />' : '<integer>' . (string)$node . '</integer>');
		}
		else
		{
			throw new uhu_LLSD_exception('Unsupported node type provided: ' . gettype($node),200);
		}
		return $doc;
	}
/**
*	Checks if the native PHP data type should be represented as a map LLSD node
*	@param mixed The native PHP data type to check
*	return bool TRUE if the native PHP data type should be represented as a LLSD node, FALSE if it should be an array. instance of uhu_LLSD_exception thrown if the type would be neither "map" nor "array".
*	@uses uhu_LLSD_exception
*/
	protected static function is_map($node)
	{
		if(is_array($node))
		{
			$keys = array_keys($node);
			foreach($keys as $key)
			{
				if(is_numeric($key) === false && ctype_digit($key) === false)
				{
					return true;
				}
			}
			return false;
		}
		else if(is_object($node) && $node instanceof Iterator)
		{
			foreach($node as $key => $val)
			{
				if(is_integer($key) === false && ctype_digit($key) === false)
				{
					return true;
				}
			}
			return false;
		}
		else
		{
			throw new uhu_LLSD_exception('Only arrays or objects implementing the Iterator interface should be passed to uhu_LLSD::is_map',106);
		}
	}
/**
*	Used for tab-based indentation of the LLSD document
*	@return string
*/
	protected static function level_based_indent($level)
	{
		if(is_null($level))
		{
			return '';
		}
		else
		{
			return "\n" . str_repeat("\t",$level);
		}
	}
/**
*	Validates the LLSD document, useful check to run prior to parsing, also allows custom DTDs to be loaded.
*	@param string the LLSD document to validate
*	@param bool|string if false, {@link uhu_LLSD::LLSD_DTD} is used, otherwise the value is passed to DOMDocument when doing validation of the LLSD document.
*	@uses uhu_LLSD::attach_DTD()
*	@uses uhu_LLSD::attach_DTD()
*	@uses DOMDocument::loadXML()
*	@uses DOMDocument::validate()
*	@uses uhu_LLSD_exception()
*/
	public static function validate($document,$custom_dtd=false)
	{
		if(self::has_DTD($document) === false)
		{
			$document = self::attach_DTD($document,$custom_dtd);
		}
		try
		{
			$dom = new DOMDocument;
			$dom->validateOnParse = false;
		}
		catch(Exception $e)
		{
			throw new uhu_LLSD_exception('Failed to create DOMDocument object: ' . (string)$e,100);
		}
		if(($dom instanceof DOMDocument) === false)
		{
			throw new uhu_LLSD_exception('Failed to create DOMDocument object.',100);
		}
		else
		{
			try
			{
				$loaded = $dom->loadXML($document,LIBXML_DTDLOAD | LIBXML_DTDVALID);
			}
			catch(Exception $e)
			{
				throw new uhu_LLSD_exception('Could not load LLSD into DOMDocument object: ' . (string)$e,100);
			}
			if($loaded)
			{
				try
				{
					$valid = $dom->validate();
				}
				catch(Exception $e)
				{
					throw new uhu_LLSD_exception('Could not run validation process on LLSD document: ' . (string)$e,102);
				}
				unset($_document,$document,$dom);
				return $valid;
			}
			else
			{
				throw new uhu_LLSD_exception('Failed to load LLSD into DOMDocument object.',100);
			}
		}
	}
/**
*	Checks if the LLSD document contains a DTD
*	@param string the LLSD document to check
*	@return bool TRUE if the document contains a DTD, FALSE otherwise.
*/
	protected static function has_DTD($document)
	{
		return (strpos($document,'<!DOCTYPE') !== false);
	}
/**
*	Attaches a DTD to the LLSD document so it can be validated. Does not currently support replacing an existing DTD. Should probably use preg_replace if '?>' starts cropping up inside string elements, though thats more likely to show up as "?&gt;", not "?>"
*	@param string the LLSD document to attach the DTD to.
*	@param bool|string if false, {@link uhu_LLSD::LLSD_DTD} is used, otherwise the value is passed to DOMDocument when doing validation of the LLSD document.
*	@return string The LLSD document, modified with the specified DTD
*	@uses uhu_LLSD::LLSD_DTD
*/
	protected static function attach_DTD($document,$custom_dtd=false)
	{
		$dtd = ($custom_dtd === false) ? self::LLSD_DTD : (string)$custom_dtd;
		return (strpos($document,'<?xml') !== false) ? str_replace('?>',"?>\n" . $dtd,$document) : '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $dtd . $document;
	}
}
?>